Add an optional class filter to JavaSerializer deserialization - #1295
Add an optional class filter to JavaSerializer deserialization#1295Nexory wants to merge 2 commits into
Conversation
JavaSerializer.read deserializes with ObjectInputStream.readObject and previously applied no restriction on which classes may be deserialized; Kryo's setRegistrationRequired does not apply on this path because the bytes are handed to the JDK serialization mechanism. Add an opt-in setClassFilter(Predicate<Class>), checked in resolveClass, so a class for which the predicate returns false is rejected with a KryoException before it is used. The default is unchanged (no filter) and the existing classloader resolution is preserved.
|
Hi @Nexory. Thanks for the PR. The issue I see is that your filter is applied to late. It does Kryo 6 will move to Java 17 or 21 and we could replace this with an |
Applied to the resolved Class, the filter could not refuse a name that does not resolve at all, and it let the class be loaded first. It now runs on the name as the first thing resolveClass does, so a refused class is never loaded. The filter type changes from Predicate<Class> to Predicate<String> accordingly. Measured with -verbose:class on a gadget nested inside an allowed outer type: with the filter refusing, the class does not appear in the load list and its static initializer does not run; with it allowing, both happen.
|
You are right that the check belongs before the class is resolved, and it now is: the filter One correction to the reasoning, because it affects how urgent this was. The old code called Measured with Worth noting for the Kryo 6 plan: this is the same shape I followed your PR workflow: build on JDK 11, then |
You are right! I overlooked the It's unfortunate that we are still on JDK8 and can't go for What's your opinion? Should we revert back to your original version, or should we go with |
What / Why
JavaSerializer.read(...)deserializes withObjectInputStream.readObject(), and itsObjectInputStreamWithKryoClassLoaderoverrides onlyresolveClass(...)for classloader resolution. It applies no restriction on which classes may be deserialized. Kryo's ownsetRegistrationRequired(true)does not help on this path, because the bytes are handed to the JDK serialization mechanism rather than resolved through Kryo's class registration, so a Kryo stream that usesJavaSerializerfor any field type deserializes arbitrary classes from the input.This adds an opt-in class filter to
JavaSerializeras defense-in-depth when reading serialized data from an untrusted source:setClassFilter(Predicate<Class>)/getClassFilter().resolveClass(...): a class for which the predicate returnsfalseis rejected with aKryoExceptionbefore it is used.Non-breaking
The default is unchanged. With no filter set (
null, the default) behavior is exactly as before, and the existing classloader-resolution logic inresolveClass(...)is preserved.Java 8 note
Kryo targets Java 8, so this uses a
Predicate<Class>checked in the existingresolveClass(...)hook rather than the Java 9+java.io.ObjectInputFilter(JEP-290). If you prefer, I am happy to additionally wire a realObjectInputFilteron Java 9+ (resolved reflectively so the Java 8 build is unaffected), or to expose aSet<String>allowlist instead of a predicate. Whichever shape you prefer, the check stays in one place.Tests
JavaSerializerTestadds two cases: a filter that disallows the class rejects deserialization (asKryoException), and a filter that allows it still round-trips. The existing tests are unchanged. Verified withmvn -Dtest=JavaSerializerTest teston JDK 11 (source/target 8).